package info.motteke.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;

/**
 * Base64のエンコード/デコードを行います。
 *
 * @author Y.Wakamatsus
 */
public class Base64 {

    private static final String DEFAULT_CHARSET = "UTF-8";

    /**
     * 6bitから文字に変換するためのテーブルです。
     */
    private static final char[] byteToChar = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();

    /**
     * 文字から6bitの値に変換するためのテーブルです。
     */
    private static final int[] charToByte;

    /**
     *
     */
    private static final int BITMASK_2 = 3;

    private static final int BITMASK_4 = 15;

    private static final int BITMASK_6 = 63;

    private static final int BITMASK_8 = 255;

    static {
        charToByte = new int[256];
        Arrays.fill(charToByte, -1);

        for (int i = 0; i < byteToChar.length; i++) {
            charToByte[byteToChar[i]] = i;
        }
    }

    public static String encode(String input) {
        return encode(input, DEFAULT_CHARSET);
    }

    public static String encode(String input, String charset) {
        try {
            return encode(input.getBytes(charset));
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static String encode(byte[] input) {
        try {
            return encode(new ByteArrayInputStream(input));
        } catch (IOException e) {
            throw new AssertionError();
        }
    }

    public static String encode(InputStream input) throws IOException {
        StringBuilder buf = new StringBuilder();
        int b;
        int byteBuf = 0;
        int byteCount = 0;

        while ((b = input.read()) != -1) {
            switch (byteCount++ % 3) {
                case 0:
                    buf.append(byteToChar[b >> 2]);
                    byteBuf = (b & BITMASK_2) << 4;
                    break;

                case 1:
                    buf.append(byteToChar[byteBuf | b >> 4]);
                    byteBuf = (b & BITMASK_4) << 2;
                    break;

                case 2:
                    buf.append(byteToChar[byteBuf | b >> 6]);
                    buf.append(byteToChar[b & BITMASK_6]);
                    byteBuf = 0;
                    break;
            }
        }
        if (byteCount % 3 != 0) {
            buf.append(byteToChar[byteBuf]);
        }

        switch (buf.length() & 0x3) {
            case 0:
                break;

            case 1:
                buf.append("===");
                break;
            case 2:
                buf.append("==");
                break;
            case 3:
                buf.append("=");
                break;
        }

        return buf.toString();
    }

    public static byte[] decode(String input) {
        return decode(input, DEFAULT_CHARSET);
    }

    public static byte[] decode(String input, String charset) {
        try {
            byte[] bytes = input.getBytes(charset);
            return decode(new ByteArrayInputStream(bytes));
        } catch (IOException e) {
            throw new AssertionError();
        }
    }

    public static byte[] decode(InputStream input) throws IOException {
        ByteArrayOutputStream os = new ByteArrayOutputStream();

        int readCount = 0;
        int byteBuf = 0;
        int n;

        while ((n = input.read()) != -1) {
            int b = charToByte[n];

            if (b == -1) {
                continue;
            }

            switch (readCount++ & 0x3) {
                case 0:
                    byteBuf = b << 2 & BITMASK_8; // 11111100
                    break;

                case 1:
                    os.write(byteBuf | b >> 4); // 11111122 2222
                    byteBuf = b << 4 & BITMASK_8; // 22220000
                    break;

                case 2:
                    os.write(byteBuf | b >> 2); // 22223333 33
                    byteBuf = b << 6 & BITMASK_8; // 33000000
                    break;

                case 3:
                    os.write(byteBuf | b); // 33444444
                    byteBuf = 0;
                    break;
            }
        }

        //処理を残したビットがあるとまずい
        assert byteBuf == 0;

        return os.toByteArray();
    }
}
